﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;

namespace Mathematics
{
    public class ExpressionValue
    {
        #region Constructors

        private ExpressionValue(string value)
        {
            Value = value;
            Type = ValueTypes.String;
            TypeName = "string";
        }

        private ExpressionValue(double value)
        {
            Value = value;
            Type = ValueTypes.Number;
            TypeName = "number";
        }

        private ExpressionValue(bool value)
        {
            Value = value;
            Type = ValueTypes.Bool;
            TypeName = "bool";
        }

        protected ExpressionValue(string typeName,object value)
        {
            if (value == null)
                throw new ArgumentNullException("value");
            if (string.IsNullOrWhiteSpace(typeName))
                throw new ArgumentNullException("typeName");

            Type = ValueTypes.Unknown;
            TypeName = typeName;
            Value = value;
        }

        private ExpressionValue()
        {
            Type = ValueTypes.Void;
            TypeName = "void";
        }

        #endregion

        #region Public Properties

        public object Value { get; private set; }
        public ValueTypes Type { get; private set; }
        public string TypeName { get; private set; }
        public bool HasValue { get { return Type != ValueTypes.Void; } }

        #endregion

        #region Helper Properties

        public bool BoolValue
        {
            get
            {
                switch (Type)
                {
                    case ValueTypes.Number:
                        return ((double)Value != 0);
                    case ValueTypes.Bool:
                        return (bool)Value;
                    case ValueTypes.String:
                        return string.IsNullOrEmpty((string) Value);
                    case ValueTypes.Void:
                        return false;
                    case ValueTypes.Unknown:
                        return ConvertToBool();
                    default:
                        throw new InvalidCastException("Unrecognized data type");
                }
            }
        }
        
        public double NumberValue
        {
            get
            {
                switch (Type)
                {
                    case ValueTypes.Number:
                        return (double)Value;
                    case ValueTypes.Bool:
                        return ((bool) Value) ? 1.0 : 0.0;
                    case ValueTypes.String:
                        double result;
                        var canParse = double.TryParse((string) Value, out result);
                        if (!canParse)
                            throw new InvalidCastException(string.Format("String value '{0}' could not be converted into a number", Value));
                        return result;
                    case ValueTypes.Void:
                        return 0.0;
                    case ValueTypes.Unknown:
                        return ConvertToNumber();
                    default:
                        throw new InvalidCastException("Unrecognized data type");
                }
            }
        }

        public string StringValue
        {
            get
            {
                switch (Type)
                {
                    case ValueTypes.Number:
                        return Value.ToString();
                    case ValueTypes.Bool:
                        return Value.ToString();
                    case ValueTypes.String:
                        return (string)Value;
                    case ValueTypes.Void:
                        return string.Empty;
                    case ValueTypes.Unknown:
                        return ConvertToString();
                    default:
                        throw new InvalidCastException("Unrecognized data type");
                }
            }
        }

        #endregion

        #region Public Methods

        public override string ToString()
        {
            switch (Type)
            {
                case ValueTypes.Number:
                    return Value.ToString();
                case ValueTypes.Bool:
                    return (bool)Value ? "true" : "false";
                case ValueTypes.String:
                    return string.Format("'{0}'", Value);
                case ValueTypes.Void:
                    return "void";
                case ValueTypes.Unknown:
                    return GetToString();
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        #endregion

        #region Local Conversions

        protected virtual bool ConvertToBool()
        {
            throw new InvalidCastException("Unknown data type cannot be converted to a bool");
        }

        protected virtual double ConvertToNumber()
        {
            throw new InvalidCastException("Unknown data type cannot be converted to a number");
        }

        protected virtual string ConvertToString()
        {
            throw new InvalidCastException("Unknown data type cannot be converted to a string");
        }

        protected virtual ExpressionValue Add(ExpressionValue other)
        {
            throw new InvalidCastException("Unknown data type cannot be added to any other values");
        }

        protected virtual ExpressionValue Subtract(ExpressionValue other)
        {
            throw new InvalidCastException("Unknown data type cannot be subtracted from any other values");
        }

        protected virtual ExpressionValue Multiply(ExpressionValue other)
        {
            throw new InvalidCastException("Unknown data type cannot be multiplied by any other values");
        }

        protected virtual ExpressionValue Divide(ExpressionValue other)
        {
            throw new InvalidCastException("Unknown data type cannot be divided by any other values");
        }

        protected virtual ExpressionValue Exponent(ExpressionValue other)
        {
            throw new InvalidCastException("Unknown data type cannot be raised by any other values");
        }
        protected virtual ExpressionValue Mod(ExpressionValue other)
        {
            throw new InvalidCastException("Unknown data type cannot have a modulus applied to it");
        }

        protected virtual string GetToString()
        {
            throw new InvalidCastException("Unknown data type cannot be converted to a string");
        }

        #endregion

        #region Operators

        public static ExpressionValue ExponentOperator(ExpressionValue v1, ExpressionValue v2)
        {
            DetectVoids(v1, v2);

            switch (v1.Type)
            {
                case ValueTypes.Number:
                    return NumberExp(v1, v2);
                case ValueTypes.Bool:
                    return BoolExp(v1, v2);
                case ValueTypes.String:
                    return StringExp(v1, v2);
                case ValueTypes.Unknown:
                    return v1.Exponent(v2);
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        public static ExpressionValue operator +(ExpressionValue v1, ExpressionValue v2)
        {
            DetectVoids(v1, v2);

            switch (v1.Type)
            {
                case ValueTypes.Number:
                    return NumberAdd(v1,v2);
                case ValueTypes.Bool:
                    return BoolAdd(v1, v2);
                case ValueTypes.String:
                    return StringAdd(v1, v2);
                case ValueTypes.Unknown:
                    return v1.Add(v2);
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        public static ExpressionValue operator -(ExpressionValue v1, ExpressionValue v2)
        {
            DetectVoids(v1, v2);

            switch (v1.Type)
            {
                case ValueTypes.Number:
                    return NumberSubtract(v1, v2);
                case ValueTypes.Bool:
                    return BoolSubtract(v1, v2);
                case ValueTypes.String:
                    return StringSubtract(v1, v2);
                case ValueTypes.Unknown:
                    return v1.Subtract(v2);
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        public static ExpressionValue operator *(ExpressionValue v1, ExpressionValue v2)
        {
            DetectVoids(v1, v2);

            switch (v1.Type)
            {
                case ValueTypes.Number:
                    return NumberMultiply(v1, v2);
                case ValueTypes.Bool:
                    return BoolMultiply(v1, v2);
                case ValueTypes.String:
                    return StringMultiply(v1, v2);
                case ValueTypes.Unknown:
                    return v1.Multiply(v2);
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        public static ExpressionValue operator /(ExpressionValue v1, ExpressionValue v2)
        {
            DetectVoids(v1, v2);

            switch (v1.Type)
            {
                case ValueTypes.Number:
                    return NumberDivide(v1, v2);
                case ValueTypes.Bool:
                    return BoolDivide(v1, v2);
                case ValueTypes.String:
                    return StringDivide(v1, v2);
                case ValueTypes.Unknown:
                    return v1.Divide(v2);
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        public static ExpressionValue operator %(ExpressionValue v1, ExpressionValue v2)
        {
            DetectVoids(v1, v2);

            switch (v1.Type)
            {
                case ValueTypes.Number:
                    return NumberMod(v1, v2);
                case ValueTypes.Bool:
                    return BoolMod(v1, v2);
                case ValueTypes.String:
                    return StringMod(v1, v2);
                case ValueTypes.Unknown:
                    return v1.Mod(v2);
                default:
                    throw new InvalidCastException("Unrecognized data type");
            }
        }

        #endregion

        #region Operator Helpers

        private static ExpressionValue NumberAdd(ExpressionValue v1,ExpressionValue v2)
        {
            var result = v1.NumberValue + v2.NumberValue;
            return new ExpressionValue(result);
        }

        private static ExpressionValue BoolAdd(ExpressionValue v1, ExpressionValue v2)
        {
            // Logic Table
            // # 0 1
            // 0 0 1
            // 1 1 1
            var val1 = v1.BoolValue;
            var val2 = v2.BoolValue;
            var result = val1 || val2;
            return new ExpressionValue(result);
        }

        private static ExpressionValue StringAdd(ExpressionValue v1, ExpressionValue v2)
        {
            var result = v1.StringValue + v2.StringValue;
            return new ExpressionValue(result);
        }

        private static ExpressionValue NumberSubtract(ExpressionValue v1, ExpressionValue v2)
        {
            var result = v1.NumberValue - v2.NumberValue;
            return new ExpressionValue(result);
        }

        private static ExpressionValue BoolSubtract(ExpressionValue v1, ExpressionValue v2)
        {
            // Logic Table
            // # 0 1
            // 0 0 0
            // 1 1 0
            var val1 = v1.BoolValue;
            var val2 = v2.BoolValue;
            var result = val1 && !val2;
            return new ExpressionValue(result);
        }

        private static ExpressionValue StringSubtract(ExpressionValue v1, ExpressionValue v2)
        {
            var result = v1.StringValue.Replace(v2.StringValue,string.Empty);
            return new ExpressionValue(result);
        }

        private static ExpressionValue NumberMultiply(ExpressionValue v1, ExpressionValue v2)
        {
            var result = v1.NumberValue * v2.NumberValue;
            return new ExpressionValue(result);
        }

        private static ExpressionValue BoolMultiply(ExpressionValue v1, ExpressionValue v2)
        {
            // Logic Table
            // # 0 1
            // 0 0 0
            // 1 0 1
            var val1 = v1.BoolValue;
            var val2 = v2.BoolValue;
            var result = val1 && !val2;
            return new ExpressionValue(result);
        }

        private static ExpressionValue StringMultiply(ExpressionValue v1, ExpressionValue v2)
        {
            var val1 = v1.StringValue;
            var val2 = v2.NumberValue;

            var result = string.Empty;
            for(int i=1;i<=val2;i++)
            {
                result += val1;
            }
            return new ExpressionValue(result);
        }

        private static ExpressionValue NumberDivide(ExpressionValue v1, ExpressionValue v2)
        {
            var result = v1.NumberValue / v2.NumberValue;
            return new ExpressionValue(result);
        }

        private static ExpressionValue BoolDivide(ExpressionValue v1, ExpressionValue v2)
        {
            // Logic Table
            // # 0 1
            // 0 X 0
            // 1 X 1
            var val1 = v1.BoolValue;
            var val2 = v2.BoolValue;
            return val2 ? new ExpressionValue(val1) : Void;
        }

        private static ExpressionValue StringDivide(ExpressionValue v1, ExpressionValue v2)
        {

            // TODO:  Split string based on values and return an array
            throw new NotImplementedException(
                "String division is not implemented in the current version of this applicaiton");
        }

        private static ExpressionValue NumberExp(ExpressionValue v1, ExpressionValue v2)
        {
            var result = Math.Pow(v1.NumberValue, v2.NumberValue);
            return new ExpressionValue(result);
        }

        private static ExpressionValue BoolExp(ExpressionValue v1, ExpressionValue v2)
        {
            // Logic Table
            // # 0 1
            // 0 1 0
            // 1 1 1
            var val1 = v1.BoolValue;
            var val2 = v2.BoolValue;
            var result = val2 ? false : true;
            return new ExpressionValue(val1);
        }

        private static ExpressionValue StringExp(ExpressionValue v1, ExpressionValue v2)
        {
            throw new NotImplementedException(
                "String exponentials are not implemented in the current version of this applicaiton");
        }

        private static ExpressionValue NumberMod(ExpressionValue v1, ExpressionValue v2)
        {
            var result =v1.NumberValue % v2.NumberValue;
            return new ExpressionValue(result);
        }

        private static ExpressionValue BoolMod(ExpressionValue v1, ExpressionValue v2)
        {
            // Logic Table
            // # 0 1
            // 0 X 1
            // 1 X 0
            var val1 = v1.BoolValue;
            var val2 = v2.BoolValue;
            return val2 ? new ExpressionValue(!val1) : Void;
        }

        private static ExpressionValue StringMod(ExpressionValue v1, ExpressionValue v2)
        {
            throw new NotImplementedException(
                "String modulus are not implemented in the current version of this applicaiton");
        }

        private static void DetectVoids(ExpressionValue v1, ExpressionValue v2)
        {
            if (v1.Type == ValueTypes.Void || v2.Type == ValueTypes.Void)
                throw new InvalidOperationException("Type 'void' cannot be included in any operations");
        }

        #endregion

        #region Static Members

        public static readonly ExpressionValue EmptyString = NewString(string.Empty);
        public static readonly ExpressionValue Void = new ExpressionValue();

        public static ExpressionValue NewNumber(double value)
        {
            return new ExpressionValue(value);
        }

        public static ExpressionValue NewBool(bool value)
        {
            return new ExpressionValue(value);
        }

        public static ExpressionValue NewString(string value)
        {
            if (value == null)
                throw new ArgumentNullException("value");

            return new ExpressionValue(value);
        }

        #endregion
    }
}
